[UE]夺旗模式框架
夺旗模式框架
描述
夺旗模式:
场景里会按照一定的规则刷新出不同类型的旗帜(不同类型的旗帜有不同的得分、血量、表现等);
玩家会被分成若干个队伍,抢夺刷新出来的旗帜,将旗帜带到自己队伍目标点进行销毁,则可以获得分数;
玩家持有旗帜时,旗帜可能会掉落或被抢夺:被其它玩家撞击到则旗帜被抢夺到其它玩家身上;旗帜的血量归零则会掉落回场景;
有正式赛与加时赛两种类型的比赛,首先会开启正式赛;对于正式赛,率先达到目标分的队伍获胜;如果平局则开启加时赛,否则进行结算;对于加时赛,率先得分的队伍获胜,如果平局则以两队平局进行结算;
要点总结:
- 旗帜实体:旗帜本身有自己的类型、得分、血量;
- 旗帜刷新:场景内根据一定规则(倒计时刷新、旗子上缴)会刷出不同类型旗帜;
- 旗帜传递:玩家可以拾取场景里的旗帜、相互抢夺旗帜、掉落旗帜(持有旗帜血量归零)、将旗帜送给终点得分等;玩家可以组队,共享队伍得分;同队伍之间的玩家可以传递旗帜;
- 单局流程:所有玩家就绪分队后,传送到起始点准备开赛;如果一局比赛平分,一定条件下可以触发下一场比赛继续;
模式基础
单局流程
首先,一个完整的夺旗赛,由若干个子比赛构成,子比赛逐渐推进;
flowchart LR M[MainProcess] R0[RaceProcess_0] R1[RaceProcess_1] R2[RaceProcess_2] M-->R0 M-->R1 M-->R2 R0-.->R1 R1-.->R2
比赛胜负规则:
单场比赛结束时,判定是否有一方队伍得分更高:
如果有,则全局结束;
如果没有,尝试进入下一场比赛(如果有配置的话),若没有下一场则全局结束;
- 正式赛:比赛结束条件:有一队达到目标分 / 倒计时结束
- 加时赛:比赛结束条件:有一队得到分数 / 倒计时结束
- 其它类型比赛……
局内玩法
从业务出发,先明确有哪些相关逻辑:
最基本的交互显然是:
flowchart LR F0[场景旗帜_0] F1[场景旗帜_1] G0([目标点_0]) G1([目标点_1]) P0[[Player_0]] P1[[Player_1]] F0--PickUp--->P0--Drop-->F0 P0--Reach--->G0 P0<--Strike--->P1
PickUp:Player触碰到场景旗帜,就可以拾取该旗帜到自己身上;
Drop:Player持有的旗帜存在血量,其血量归零(可能是被其它Player攻击导致扣血),就会触发旗帜的掉落,在对应位置生成场景旗帜;
Reach:Player将持有的旗帜护送到目标点,获得分数,同时该旗帜会被销毁;
Strike:Player撞击另一个Player,可以抢夺对方的旗帜;
业务拆分
首先,先给出基本的一些元素定义;
游戏玩法的本质是由刷新点刷新出旗帜,旗帜在各个部分之间交互传递,旗帜本身定义为
FlagItem。旗帜在场景(不在
Player身上)里的表现为,存在对应的FlagSceneItem。场景里存在若干个终点,定义该终点为
GoalPointSceneItem
这里说的 交互 的本质实际上是一个 Flag 的 Instance 在 各个部分 之间的 传递 。想一下所谓的 各个部分,到底是什么?
本质可以抽象为一个个持有 Flag 的容器,将其定义为 FlagContainer。
这里显然有三种 Container:
Container_WorldFlag:定义WorldFlag为 没有被Player持有的旗帜
Container_GoalPoint:定义GoalPoint为目标点
Container_Player:每个Player持有一个Container;
细想一下, WorldFlag 、GoalPoint 的具体业务逻辑和具体的某个 Instance 没有太大关系,只需要通过 ID 串联即可,所以对于 WorldFlag、GoalPoint,不需要真的在每一个 Instance 都创建对应 Container,只需要一个单局管理的唯一 Manager 用来代替管理即可。但显然对于每个 Player 都需要有自己的 Container。
可以在单局内创建全局唯一的 Container_WorldFlag 、Container_GoalPoint 实例。
flowchart TD
F0[FlagSceneItem_0]
F1[FlagSceneItem_1]
F2[FlagSceneItem_2]
subgraph WorldFlag[Container_WorldFlag]
direction LR
F0-.-F1-.-F2
end
G0([GoalPoint_0])
G1([GoalPoint_1])
G2([GoalPoint_2])
subgraph GoalPoint[Container_GoalPoint]
direction LR
G0-.-G1-.-G2
end
P0[[Container_Player_0]]
P1[[Container_Player_1]]
WorldFlag--PickUp--->P0--Drop--->WorldFlag
P0--Reach--->GoalPoint
P0<--Strike--->P1
这样就可以将 交互 的逻辑拆得非常简单了:
PickUp:由FlagSceneItem和Player的Overlap触发,将Container_WorldFlag持有的对应该FlagSceneItem的FlagItem,传递给这个Player的Conatiner_Player
Drop: 由Player的血量变化归零触发,将Container_Player上的所有FlagItem转移给Cotainer_WorldFlag
Reach:由Player和GoalPointSceneItem的Overlap触发,将Container_Player上的所有FlagItem转移给Cotainer_GoalPonit
Strike:由Player0和Player1的撞击触发,将Container_Player上的所有FlagItem转移给Cotainer_Player1
这里的 PickUp、Drop、Reach、Strike 是这张图的 边,将其定义为 TransferPolicy。
对于 Container,有各自的 Add(FlagItem)、Remove(FlagItem),一些基本逻辑:
Container_WorldFlag:
AddItem:从FlagItem上取出位置,将Item血量设置为满血,通过调用FlagSceneItemManager创建出FlagItem对应的FlagSceneItem;
RemoveItem:调用FlagSceneItemManager删除对应的FlagItem对应的FlagSceneItem;
Container_GoalPoint:
AddItem:判断是从哪个Container来的,给对应Player加分,并且RemoveItem;
RemoveItem:销毁对应的FlagItem;
Container_Player:
AddItem:从FlagItem获得FlagSyncData同步给自己,并设置Player血量为Item的血量等逻辑;
RemoveItem:将Player血量设置给Item血量,清空同步信息等逻辑;
完整的业务拆分完毕,这下需要哪些系统显而易见:
ProcessSystem:负责管理单局流程,创建比赛与推动玩法进程;
TeamSystem:负责Player的组队,管理Memebr、Score等;
FlagItemSystem:负责管理所有FlagItem实例,提供Create、Destroy等方法,维护Item基本的生命周期;
ContainerSystem:负责提供基本的CreateContainer、DestoryContainer方法,对于具体的Container,需要支持AddItem、RemoveItem、GetItem等;
TransferSystem:负责管理所有的TransferPolicy,提供TransferItem方法;
FlagSpawnerSystem:旗帜的刷新点,负责WorldFlag的生成,生成FlagItem并Add到Container_WorldFlag;
SceneItemSystem:维护场景里SceneItem的生成,提供Create、Destroy对应的SceneItem的方法;对于某个具体的SceneItem,需要实现基础的Overlap、Sync;
具体实现
ProcesSystem
首先需要一个挂在 GameState 上的 MainProcess 负责管理全局的流程,串联多个 RaceProcess 推进玩法;
显然,一个最简单的流程可以这样:
flowchart LR M[MainProcess] R0[RaceProcess_0] R1[RaceProcess_1] F[Finish] M---->|1. 开赛|R0 R0-.->|2. Result_0|M M-->|3. Check_Result_0|R1 R1-.->|4. Result_1|M M-->|5. Check_Result_1|F R0-.->R1 R1-.->F
但是这样循环流程需要在 Main 里面关注 RaceProcess 给出的 Result 事件,显然有点不够优雅。
发现这里的主要由两部分组成:
RaceProcess管理子比赛流程,给出ResultMainProcess接受子比赛Result,校验并判断开启下一场Race或者结束比赛
所以可以将其拆成两个 Controller:
MainProcess 负责提供 Dispatch 的功能,
FinishRace_Controller:监听Dispatcher的开赛事件,并向Dispatcher分发单局结束事件;CreateRace_Controller:监听单局结束事件,进行处理并向Dispatcher分发开赛事件;
flowchart TB M[MainProcess] D[Dispatcher] F[FinishRace_Controller] C[CreateRace_Controller] M-->D F-->|Send_Finsih|D D-.->|Register_Create|F C-->|Send_Create|D D-.->|Register_Finish|C
TeamSystem
支持玩家的 加入、退出 队伍,以及维护队伍的各种数据(比如 Members、Score),并维护数据,进行同步。
SceneItemSystem
classDiagram
UCFSceneItemUtils..>UCFSceneItemManager
UCFSceneItemManager..>UCFScecneItemBase
class UCFSceneItemManager {
frien UCFSceneItemUtils
- CreateItem(Type, UID, Params)
- DestroyItem(UID)
}
UCFScecneItemBase..*UCFScecneItemSyncComponent
UCFScecneItemBase..*UCFScecneItemDisplayComponent
class UCFScecneItemBase {
SyncComponent : UCFScecneItemSyncComponent
OverlapDelegate
# OnInit()
# OnUninit()
# BeginDetect()
# EndDetect()
+ CollectSyncData()
}
UCFScecneItem_WorldFlag--|>UCFScecneItemBase
UCFScecneItem_GoalPoint--|>UCFScecneItemBase
首先,需要 UCFSceneItemManager 负责管理所有的 SceneItem,同时通过 UCFSceneItemUtils 提供方法给外部调用。
对于一个具体的 SceneItem,额外对其提供两个 Component,
SyncComponent:在DS/Client生成,维护SceneItem的同步数据,这一部分数据不会被AOI裁剪,保证远距离的数据同步;DisplayComponent:仅在Client生成,随着SceneItem被AOI,用于实现客户端表现。
FlagItemSystem、ContainerSystem
classDiagram
direction LR
UCaptureFlagItemSystem..>FCaptureFlagItem
class UCaptureFlagItemSystem {
ItemInstances : TMap~uint64|TSharedPtr~FCaptureFlagItem~~
+ CreateItem(ItemID)
+ DestroyItem(ItemUID)
+ GetItem(ItemUID)
- GenerateUID()
- RegisterClearTimer()
- UnregisterClearTimer()
- ClearAllItems(bCheckUnused)
}
%% ----- FlagItem -----
namespace FlagItem {
class FCaptureFlagItem
class FCaptureFlagItemSyncData
}
FCaptureFlagItem..FCaptureFlagItemSyncData
FCaptureFlagItem..UCaptureFlagContainerBase
class FCaptureFlagItem {
+ InitItem(InUID, ItemID)
+ GetSyncData() : FCaptureFlagItemSyncData
+ UID / ItemID / Type / Health / MaxHealth...
+ Container : TWeakObjectPtr~UCaptureFlagContainerBase~
+ TransferReason : ECFTransferReason
}
class FCaptureFlagItemSyncData {
+ UID / ItemID / Type / Health / MaxHealth / TransferReason
}
%% ----- FlagContainer -----
namespace FlagContainer {
class UCaptureFlagContainerBase
class UCaptureFlagContainer_WorldFlag
class UCaptureFlagContainer_GoalPoint
class UCaptureFlagContainer_Player
}
class UCaptureFlagContainerBase {
Items : TMap ~uint64|TWeakPtr~FCaptureFlagItem~~
Owner : TWeakObjectPtr~UObject~
+ Init(InOwner)
+ Uninit()
+ AddItem(TWeakPtr~FCaptureFlagItem~Item, Params)
+ RemoveItem(TWeakPtr~FCaptureFlagItem~ Item)
+ VerifyCanAddItem()
+ GetType() : ECFContainerType
# OnInit()
# OnUninit()
# OnAddItem(TWeakPtr~FCaptureFlagItem~Item, Params)
# OnRemoveItem(TWeakPtr~FCaptureFlagItem~ Item)
}
UCaptureFlagContainer_WorldFlag--|>UCaptureFlagContainerBase
UCaptureFlagContainer_GoalPoint--|>UCaptureFlagContainerBase
UCaptureFlagContainer_Player--|>UCaptureFlagContainerBase
class UCaptureFlagContainer_Player{
# GetType() : ECFContainerType
}
%% ----- ContainerOwner -----
namespace ContainerOwner {
class UCaptureFlagManager
class UCaptureFlagComponent
class ICaptureFlagContainerOwnerInterface
}
UCaptureFlagManager..>UCaptureFlagContainer_GoalPoint
UCaptureFlagManager..>UCaptureFlagContainer_WorldFlag
UCaptureFlagManager..|>ICaptureFlagContainerOwnerInterface
class UCaptureFlagManager{
- WorldFlagContainer
- GoalPointContainer
+ GetWorldFlagContainer()
+ GetGoalPointContainer()
}
UCaptureFlagComponent..|>ICaptureFlagContainerOwnerInterface
UCaptureFlagComponent..>UCaptureFlagContainer_Player
UCaptureFlagComponent..>FCaptureFlagItemSyncData
class UCaptureFlagComponent{
- FlagContainer_Player
- FlagItemSyncData : FCaptureFlagItemSyncData
+ GetCaptureFlagContainer()
+ UpdateFlagItemSyncData(SyncData)
}
ICaptureFlagContainerOwnerInterface..>UCaptureFlagContainerBase
class ICaptureFlagContainerOwnerInterface{
CreateFlagContainer(InOwner)
DestroyFlagContainer(Container)
}
首先,需要一个 FlagItemSystem 负责管理所有的 FlagItem 实例。提供基本的创建、销毁、查询功能的同时,这个 FlagItemSystem需要 RegisterClearTimer,每隔一段时间,Clear 不被任何一个 Container 持有的 Item。
1 | // UCaptureFlagItemManager |
对于 FlagItem,保存 Flag 最基本的信息,同时提供一个 CollectSyncData,用于生成对应的 FlagSyncData,表示需要同步的数据,在 ContainerOwner 需要的时候进行数据同步。
1 | // FlagItem.h |
需要 Container 来持有 FlagItem,在 AddItem 和 RemoveItem 内写具体的逻辑。
每种 Container 通过重载 UCaptureFlagContainerBase 的 PURE_VIRTUAL 方法 GetType 来定义其类型。
1 | // ContainerBase.h |
1 | // ContainerBase.cpp |
每个 Container 需要一个 ContainerOwner 来持有,这里有三种 Container。
Container_WorldFlag: 由CaptureFlagManager(GS)持有,一场子比赛持有一个;Container_GoalPoint:由CaptureFlagManager(GS)持有,一场子比赛持有一个;Container_Player:由CaptureFlagComponent(PS)持有,每个Player持有一个;
提供一个 ICaptureFlagContainerOwnerInterface 用于支持 Create、Destroy 对应的 Container 方法;
1 | // ContainerOwner.h |
TransferSystem
classDiagram
direction LR
UCFTransferSystem..>UCFTransferPolicyBase
UCFTransferSystem..UCaptureFlagContainerBase
class UCFTransferSystem {
Policies : FGameplaySubSystemCollection~UCFTransferPolicyBase~
friend UCFTransferPolicyBase
- TransferItem(Item, TargetContainer, TransferReason)
}
UCFTransferPolicyBase..UCaptureFlagContainerBase
class UCFTransferPolicyBase{
# OnInit()
# OnUninit()
# TransferItem(Item, TargetContainer)
# GetTransferReason() : ECFTransferReason
}
UCFTansferPolicy_PickUp--|>UCFTransferPolicyBase
UCFTansferPolicy_Drop--|>UCFTransferPolicyBase
UCFTansferPolicy_Reach--|>UCFTransferPolicyBase
UCFTansferPolicy_Strike--|>UCFTransferPolicyBase
首先需要一个 UCFTransferSystem 持有 Polices,管理并维护所有 TransferPolicy 的生命周期,具体可以参考 [UE] GameplaySubSystem 简单实现。
对于一个具体的 TransferPolicy,通过重载 TransferPolicyBase 的 PURE_VIRTUAL 方法 GetTransferReason ,明确其对应的 TransferReason,在其 OnInit、OnUninit,监听该类型需要的事件,通过对应的 Callback 调用到 Base 提供的 TransferItem 方法,执行 Item 的转移。
对于一次成功的 Transfer,显然会调用到 SourceContainer 的 Remove ,以及 TargetContainer 的 Add。
1 | // TransferManager |
1 | // TransferPolicyBase.h |
1 | // TransferPolicy.cpp |


